package org.gap.jseed.injection;

import static java.lang.String.format;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;

import org.gap.jseed.ClassNameService;
import org.gap.jseed.reflect.MethodIterator;
import org.gap.jseed.utils.JSeedDefect;

public class ClassWriter {
	private CtClass ctClass;
	private CtClass newClass;
	private final Class<?> originalClass;
	private Map<Method, MethodWriter> methodWriters;

	public ClassWriter(Class<?> class1) {
		this.originalClass = class1;
		try {
			attemptTwiceToInstantiateSubClass();
		} catch (NotFoundException e) {
			throw new JSeedDefect("Something terrible has happend while trying to create class", e);
		}

		initializeMethodWriters();
	}

	private void attemptTwiceToInstantiateSubClass() throws NotFoundException {
		try {
			ctClass = ClassPool.getDefault().get(originalClass.getName());
			newClass = createSubClass(ctClass.getName());
		} catch (RuntimeException re) {
			System.err.println("The autogenerated class of the same name already exists in the classloader [" + ctClass.getName() + "]\n and then comes up frozen.  Will try again");
			ctClass = ClassPool.getDefault().get(originalClass.getName());
			newClass = createSubClass(ctClass.getName());
		}
	}

	private void initializeMethodWriters() {
		methodWriters = new HashMap<Method, MethodWriter>();
		for (Method each : new MethodIterator(originalClass)) {
			methodWriters.put(each, new MethodWriter(each));
		}
	}

	private CtClass createSubClass(String name) {
		CtClass newClass = null;
		String newName = createName(name);
		if (!ctClass.isInterface()) {
			newClass = ClassPool.getDefault().makeClass(newName, ctClass);
		} else {
			newClass = ClassPool.getDefault().makeClass(newName);
			newClass.addInterface(ctClass);
			try {
				CtConstructor c = new CtConstructor(new CtClass[0], newClass);
				c.setBody("{}");
				c.setModifiers(Modifier.PUBLIC);
				newClass.addConstructor(c);
			} catch (CannotCompileException e) {
				e.printStackTrace();
			}
		}
		return newClass;
	}
	
	public Class<?> getOriginalClass() {
		return originalClass;
	}

	public void writeBody(Method method, String source) {
		ensureMethodWriterIsRegistered(method);
		methodWriters.get(method).setBody(source);
	}
	

	public void ensureReturn(Method method, boolean state) {
		ensureMethodWriterIsRegistered(method);
		methodWriters.get(method).ensureReturn(state);
	}

	private void ensureMethodWriterIsRegistered(Method method) {
		if (!methodWriters.containsKey(method)) {
			methodWriters.put(method, new MethodWriter(method));
		}
	}

	public void insertBefore(Method method, String code) {
		ensureMethodWriterIsRegistered(method);
		methodWriters.get(method).insertBefore(code);
	}

	public void insertAfter(Method method, String code) {
		ensureMethodWriterIsRegistered(method);
		methodWriters.get(method).insertAfter(code);
	}

	public String createName(String name) {
		String classNameServiceId = System.getProperty("org.gap.jseed.ClassNameService");
		if (classNameServiceId != null && !classNameServiceId.isEmpty()) {
			try {
				ClassNameService service = (ClassNameService)Class.forName(classNameServiceId).newInstance();
				return service.createName(name);
			} catch (InstantiationException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		return name + new Random().nextLong();
	}

	public void addField(Class<?> theType, String name) {
		try {
			CtField ctField = CtField.make(format("private %s %s;", theType.getName(), name), newClass);
			newClass.addField(ctField);
		} catch (CannotCompileException e) {
			if (name == "_jSeedContainer") {
				return;
			}
			throw new RuntimeException(e);
		}
	}

	public void addFieldWithDefaultValue(Class<?> theType, String name, String initialization) {
		try {
			CtField ctField = CtField.make(format("private %s %s;", theType.getName(), name), newClass);
			newClass.addField(ctField, initialization);
		} catch (CannotCompileException e) {
			throw new RuntimeException(e);
		}
	}

	public void addSetterMethod(String name, Class<?> property, Class<? extends Annotation> annotation) {
		addField(property, name);
		try {
			CtField field = newClass.getField(name);
			CtMethod method = CtNewMethod.setter("set" + name, field);
			AnnotationsAttribute attr = createAnnotation(annotation);
			method.getMethodInfo().addAttribute(attr);
			newClass.addMethod(method);
		} catch (Exception nfe) {
			if (name == "_jSeedContainer") {
				return;
			}
			throw new RuntimeException(nfe);
		}
	}

	public void addMethod(String src) {
		try {
			CtMethod method = CtNewMethod.make(src, newClass);
			newClass.addMethod(method);
		} catch (Exception nfe) {
			throw new RuntimeException(nfe);
		}
	}
	
	private AnnotationsAttribute createAnnotation(Class<? extends Annotation> annotation) {
		ClassFile cf = newClass.getClassFile();
		ConstPool cp = cf.getConstPool();
		AnnotationsAttribute attr = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
		javassist.bytecode.annotation.Annotation a = new javassist.bytecode.annotation.Annotation(annotation.getName(), cp);
		attr.setAnnotation(a);
		cf.setVersionToJava5();
		return attr;
	}

	public boolean isFieldExisting(String fieldName) {
		try {
			newClass.getField(fieldName);
			return true;
		} catch (NotFoundException e) {
			return false;
		}
	}

	public boolean isFieldPrimitiveType(String fieldName) {
		try {
			CtClass field = newClass.getField(fieldName).getType();
			if (field.getName().equals("int"))
				return true;
			else if (field.getName().equals("boolean"))
				return true;
			else if (field.getName().equals("long"))
				return true;
			else if (field.getName().equals("char"))
				return true;
			else if (field.getName().equals("double"))
				return true;
			else if (field.getName().equals("float"))
				return true;
			else if (field.getName().equals("byte"))
				return true;
			return false;
		} catch (NotFoundException e) {
			e.printStackTrace();
			return false;
		}
	}

	public CtClass getFieldType(String fieldName) {
		try {
			return newClass.getField(fieldName).getType();
		} catch (NotFoundException e) {
			e.printStackTrace();
			return null;
		}
	}

	public boolean isMethodExisting(String name) {
		for (CtMethod each : newClass.getMethods()) {
			if (each.getName().equals(name)) {
				return true;
			}
		}
		return false;
	}

	public void newPrivateMethod(String methodName, String methodBody, List<Class<?>> parameters) {
		String signature = "private void " + methodName + "(" + makeParameterList(parameters) + ") ";
		String src = signature + methodBody;
		try {
			CtMethod newMethod = CtMethod.make(src, newClass);
			newClass.addMethod(newMethod);
		} catch (CannotCompileException e) {
			throw new JSeedDefect("Method [" + methodName + "] could not be created: " + e.getMessage() + "\n " + src);
		}
	}

	private String makeParameterList(List<Class<?>> parameters) {
		String result = "";
		int variableName = 'a';
		for (Class<?> each : parameters) {
			if (result.isEmpty()) {
				result = each.getName() + " " + (char) variableName;
			} else {
				result += ", " + each.getName() + " " + (char) variableName;
			}
			variableName++;
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	public <T> Class<T> toJavaClass() {
		try {
			for (MethodWriter each : methodWriters.values()) {
				each.writeMethod(newClass);
			}
			newClass.setModifiers(Modifier.PUBLIC);
			Class<T> result = newClass.toClass();
			return result;
		} catch (CannotCompileException e) {
			throw new JSeedDefect("New class was not created for " + originalClass.getName() + ": " + e);
		}
	}
}
